﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using iTextSharp.text.pdf;
using PDFPatcher.Common;
using PDFPatcher.Model;

namespace PDFPatcher.Processor
{
	internal static class PdfHelper
	{
		internal static readonly int[] CompoundTypes = new int[] { PdfObject.DICTIONARY, PdfObject.ARRAY, PdfObject.STREAM };

		private static readonly DualKeyDictionary<PdfName, string> __PdfNameMap;
		private static readonly Dictionary<string, byte[]> __PdfPasswordBox = new Dictionary<string,byte[]> (StringComparer.OrdinalIgnoreCase);

		/// <summary>
		/// 切换强制读取加密文档模式。
		/// </summary>
		/// <param name="unethicalreading">是否打开强制读取模式。</param>
		internal static void ToggleUnethicalMode (bool unethicalreading) {
			PdfReader.unethicalreading = unethicalreading;
		}
		/// <summary>
		/// 切换容错模式（忽略 PDF 文档的错误）。
		/// </summary>
		/// <param name="debugMode">是否打开容错模式。</param>
		internal static void ToggleReaderDebugMode (bool debugMode) {
			PdfReader.debugmode = debugMode;
		}

		/// <summary>
		/// 打开 PDF 文件，在有需要时提示输入密码。
		/// </summary>
		/// <param name="sourceFile">需要打开的 PDF 文件。</param>
		/// <param name="partial">是否仅打开文件的部分内容。</param>
		/// <returns><see cref="PdfReader"/> 实例。</returns>
		internal static PdfReader OpenPdfFile (string sourceFile, bool partial, bool removeUnusedObjects) {
			byte[] password;
			__PdfPasswordBox.TryGetValue (sourceFile, out password);
			while (true) {
				try {
					if (File.Exists (sourceFile) == false) {
						throw new FileNotFoundException (String.Concat ("找不到文件：", sourceFile));
					}
					PdfReader r;
					if (partial) {
						r = new PdfReader (new RandomAccessFileOrArray (sourceFile), password);
					}
					else {
						r = new PdfReader (sourceFile, password);
					}
					if (password != null && password.Length > 0) {
						__PdfPasswordBox[sourceFile] = password;
					}
					if (removeUnusedObjects) {
						r.RemoveUnusedObjects ();
					}
					return r;
				}
				catch (iTextSharp.text.exceptions.BadPasswordException) {
					var f = new PasswordEntryForm (sourceFile);
					if (f.ShowDialog () == System.Windows.Forms.DialogResult.Cancel) {
						throw new iTextSharp.text.exceptions.BadPasswordException ("密码错误，没有权限打开 PDF 文件。");
					}
					password = Encoding.Default.GetBytes (f.Password);
				}
			}
		}

		internal static bool ConfirmUnethicalMode (this PdfReader pdf) {
			ToggleUnethicalMode (false);
			var r = pdf.IsOpenedWithFullPermissions
					|| FormHelper.YesNoBox (Messages.UserRightRequired) == System.Windows.Forms.DialogResult.Yes;
			ToggleUnethicalMode (true);
			return r;
		}

		internal static MuPdfSharp.MuDocument OpenMuDocument (string sourceFile) {
			byte[] password;
			__PdfPasswordBox.TryGetValue (sourceFile, out password);
			while (true) {
				var r = new MuPdfSharp.MuDocument (sourceFile, password != null ? Encoding.Default.GetString (password) : String.Empty);
				if (password != null && password.Length > 0) {
					__PdfPasswordBox[sourceFile] = password;
				}
				if (r.NeedsPassword) {
					var f = new PasswordEntryForm (sourceFile);
					if (f.ShowDialog () == System.Windows.Forms.DialogResult.Cancel) {
						throw new iTextSharp.text.exceptions.BadPasswordException ("密码错误，没有权限打开 PDF 文件。");
					}
					password = Encoding.Default.GetBytes (f.Password);
				}
				return r;
			}
		}

		static PdfHelper () {
			__PdfNameMap = InitPdfNameMap ();
		}

		private static DualKeyDictionary<PdfName, string> InitPdfNameMap () {
			var m = new DualKeyDictionary<PdfName, string> ();

			m.Add (PdfName.PAGELAYOUT, Constants.PageLayout);

			m.Add (PdfName.PAGEMODE, Constants.PageMode);

			m.Add (PdfName.DIRECTION, Constants.ViewerPreferencesType.Direction);

			m.Add (PdfName.ST, Constants.PageLabelsAttributes.StartPage);
			m.Add (PdfName.P, Constants.PageLabelsAttributes.Prefix);
			m.Add (PdfName.S, Constants.PageLabelsAttributes.Style);

			m.Add (PdfName.XYZ, Constants.DestinationAttributes.ViewType.XYZ);
			m.Add (PdfName.FIT, Constants.DestinationAttributes.ViewType.Fit);
			m.Add (PdfName.FITB, Constants.DestinationAttributes.ViewType.FitB);
			m.Add (PdfName.FITBH, Constants.DestinationAttributes.ViewType.FitBH);
			m.Add (PdfName.FITBV, Constants.DestinationAttributes.ViewType.FitBV);
			m.Add (PdfName.FITH, Constants.DestinationAttributes.ViewType.FitH);
			m.Add (PdfName.FITR, Constants.DestinationAttributes.ViewType.FitR);
			m.Add (PdfName.FITV, Constants.DestinationAttributes.ViewType.FitV);

			m.Add (PdfName.GOTO, Constants.ActionType.Goto);
			m.Add (PdfName.GOTOR, Constants.ActionType.GotoR);
			m.Add (PdfName.LAUNCH, Constants.ActionType.Launch);
			m.Add (PdfName.URI, Constants.ActionType.Uri);

			return m;
		}

		internal static string GetTypeName (int t) {
			switch (t) {
				case PdfObject.ARRAY: return "array";
				case PdfObject.BOOLEAN: return "bool";
				case PdfObject.DICTIONARY: return "dictionary";
				case PdfObject.INDIRECT: return "reference";
				case PdfObject.NAME: return "name";
				case PdfObject.NULL: return "null";
				case PdfObject.NUMBER: return "number";
				case PdfObject.STREAM: return "stream";
				case PdfObject.STRING: return "string";
				default: return String.Empty;
			}
		}

		/// <summary>
		/// 获取友好的 PdfName 文本。
		/// </summary>
		/// <param name="n"></param>
		/// <returns></returns>
		internal static string GetPdfFriendlyName (PdfName name) {
			return (__PdfNameMap.ContainsKey (name) ? __PdfNameMap[name] : GetPdfNameString(name));
		}
		/// <summary>
		/// 解析 PdfName。
		/// </summary>
		/// <param name="friendlyName">从 <seealso cref="GetPdfFriendlyName"/> 转换所得的 PdfName 说明文本。</param>
		/// <returns>与文本说明对应的 PdfName。</returns>
		internal static PdfName ResolvePdfName (string friendlyName) {
			return (__PdfNameMap.ContainsValue (friendlyName) ? __PdfNameMap.GetKeyByValue(friendlyName) : new PdfName(friendlyName));
		}

		internal static string DecodeKeyName (object name) {
			if (name is PdfName) {
				return PdfName.DecodeName (name.ToString ());
			}
			else
				return name.ToString ();
		}

		/// <summary>
		/// 获取 PDF 页面引用与页数的映射关系表。
		/// </summary>
		/// <param name="reader">源 PDF 文档。</param>
		/// <returns>键为 <see cref="PdfIndirectReference"/> 的数值，值为页数的字典。</returns>
		internal static Dictionary<int, int> GetPageRefMapper (this PdfReader reader) {
			int numPages = reader.NumberOfPages;
			var pages = new Dictionary<int, int> (numPages);
			for (int k = 1; k <= numPages; ++k) {
				pages[reader.GetPageOrigRef (k).Number] = k;
				reader.ReleasePage (k);
			}
			return pages;
		}

		/// <summary>
		/// 解析形如“D:20111021090818+08'00'”的日期格式。
		/// </summary>
		/// <param name="date">日期格式。</param>
		/// <returns></returns>
		internal static DateTimeOffset ParseDateTime (string date) {
			if (date == null
				|| date.Length != 23 && date.Length != 16
				|| date.StartsWith ("D:") == false) {
				return DateTimeOffset.MinValue;
			}
			try {
				return new DateTimeOffset (
					date.Take (2, 4).ToInt32 (), date.Take (6, 2).ToInt32 (), date.Take (8, 2).ToInt32 (),
					date.Take (10, 2).ToInt32 (), date.Take (12, 2).ToInt32 (), date.Take (14, 2).ToInt32 (),
					new TimeSpan (date.Take (16, 3).ToInt32 (), date.Take (20, 2).ToInt32 (), 0)
					);
			}
			catch (Exception) {
				return DateTimeOffset.MinValue;
			}
		}

		//internal static int GetNumber (this PdfIndirectReference indirect) {
		//    PdfDictionary pdfObj = (PdfDictionary)PdfReader.GetPdfObjectRelease (indirect);
		//    if (pdfObj.Contains (PdfName.TYPE) && pdfObj.Get (PdfName.TYPE).Equals (PdfName.PAGES) && pdfObj.Contains (PdfName.KIDS)) {
		//        PdfArray kids = (PdfArray)pdfObj.Get (PdfName.KIDS);
		//        indirect = (PdfIndirectReference)kids.ArrayList[0];
		//    }
		//    return indirect.Number;
		//}

		/// <summary>
		/// 获取解码后的 PDF 名称字符串。
		/// </summary>
		/// <param name="name">需要解码的 PDF 名称。</param>
		/// <returns>解码后的 PDF 名称字符串</returns>
		internal static string GetPdfNameString(PdfName name) {
			return GetPdfNameString (name, PdfName.DefaultEncoding);
		}

		/// <summary>
		/// 获取解码后的 PDF 名称字符串。
		/// </summary>
		/// <param name="name">需要解码的 PDF 名称。</param>
		/// <param name="encoding">用于解码的文本编码。</param>
		/// <returns>解码后的 PDF 名称字符串</returns>
		internal static string GetPdfNameString (PdfName name, Encoding encoding) {
			var s = name.ToString ();
			int len = s.Length;
			var buf = new byte[len];
			int l = 0;
			for (int k = 1; k < len; ++k) {
				char c = s[k];
				if (c == '#') {
					buf[l] = (byte)((PRTokeniser.GetHex (s[++k]) << 4) + PRTokeniser.GetHex (s[++k]));
				}
				else {
					buf[l] = (byte)c;
				}
				l++;
			}
			return (encoding ?? System.Text.Encoding.Default).GetString (buf, 0, l);
		}

		/// <summary>
		/// 获取页面可见的边框。
		/// </summary>
		/// <param name="page">页面字典。</param>
		/// <returns>页面的可见边框。</returns>
		internal static iTextSharp.text.Rectangle GetPageVisibleRectangle (this PdfDictionary page) {
			if (page == null) {
				return null;
			}
			PdfArray box;
			var c = new float[4];
			if ((page.Contains (PdfName.CROPBOX) && (box = page.GetAsArray (PdfName.CROPBOX)) != null && box.Size == 4)
				|| (page.Contains (PdfName.MEDIABOX) && (box = page.GetAsArray (PdfName.MEDIABOX)) != null && box.Size == 4)) {
				for (int i = 0; i < 4; i++) {
					c[i] = box.GetAsNumber (i).FloatValue;
				}
				var rect = new iTextSharp.text.Rectangle (c[0], c[1], c[2], c[3]);
				var r = page.GetAsNumber (PdfName.ROTATE);
				if (r != null) {
					return new iTextSharp.text.Rectangle (c[0], c[1], c[2], c[3], r.IntValue);
				}
				return rect;
			}
			else
				return null;
		}

		internal static void ClearPageLinks (this PdfReader r) {
			int pageCount = r.NumberOfPages + 1;
			for (int i = 1; i < pageCount; i++) {
				ClearPageLinks (r, i);
			}
		}

		internal static void ClearPageLinks (this PdfReader r, int pageNumber) {
			r.ResetReleasePage ();
			var pageDic = r.GetPageN (pageNumber);
			var annots = (PdfArray)PdfReader.GetPdfObjectRelease (pageDic.Get (PdfName.ANNOTS));
			if (annots == null) {
				r.ReleasePage (pageNumber);
			}
			else {
				IList<PdfObject> arr = annots.ArrayList;
				for (int j = arr.Count - 1; j >= 0; j--) {
					var item = arr[j];
					var annot = (PdfDictionary)PdfReader.GetPdfObjectRelease (item);
					if (PdfName.LINK.Equals (annot.Get (PdfName.SUBTYPE))) {
						arr.RemoveAt (j);
					}
				}
				if (annots.ArrayList.Count == 0) {
					pageDic.Remove (PdfName.ANNOTS);
				}
			}
			r.ResetReleasePage ();
		}

		internal static string GetValidXmlString (string value) {
			if (String.IsNullOrEmpty (value)) {
				return String.Empty;
			}
			var marks = new List<int> (3);
			int p = 0;
			foreach (char c in value) {
				switch (c) {
					case '\t':
					case '\r':
					case '\n':
						break;
					default:
						// invalid character
						if (Char.IsControl (c)) {
							if (marks.Count == 0) {
								marks.Add (0);
							}
							marks.Add (p+1);
						}
						break;
				}
				p++;
			}
			if (marks.Count > 0) {
				marks.Add (value.Length + 1);
			}
			if (marks.Count > 1) {
				var sb = new StringBuilder ();
				for (int i = 1; i < marks.Count; i++) {
					if (i > 1) {
						sb.Append (' ');
					}
					sb.Append (value, marks[i - 1], marks[i] - 1 - marks[i - 1]);
				}
				return sb.ToString ();
			}
			else {
				return value;
			}
		}

		internal static string GetArrayString (PdfArray array) {
			return GetArrayString (array.ArrayList);
		}

		internal static string GetArrayString (ICollection<PdfObject> array) {
			var sb = new StringBuilder ();
			int k = 0;
			foreach (var item in array) {
				if (++k > 1) {
					sb.Append (' ');
				}
				if (item.Type == PdfObject.ARRAY) {
					sb.Append ('[');
					sb.Append (GetArrayString (item as PdfArray));
					sb.Append (']');
				}
				else if (item.Type == PdfObject.DICTIONARY || item.Type == PdfObject.STREAM) {
					sb.Append ("<<...>>");
				}
				else {
					sb.Append (item);
				}
			}
			return sb.ToString ();
		}

		internal static string GetNumericArrayString (PdfArray a, float unitFactor) {
			var sb = new StringBuilder ();
			for (int k = 0; k < a.ArrayList.Count; k++) {
				if (k != 0) {
					sb.Append (' ');
				}
				if (a.GetAsArray (k) != null) {
					sb.Append ('[');
					sb.Append (a.GetAsArray (k));
					sb.Append (']');
				}
				else {
					var o = a.ArrayList[k];
					sb.Append (o.Type == PdfObject.NUMBER ? UnitConverter.FromPoint (o.ToString(), unitFactor) : o.ToString ());
				}
			}
			return sb.ToString ();
		}

		internal static void Put (this PdfDictionary dict, PdfName key, string value) {
			dict.Put (key, value.ToPdfString ());
		}
		internal static void Put (this PdfDictionary dict, PdfName key, int value) {
			dict.Put (key, new PdfNumber(value));
		}
		internal static void Put (this PdfDictionary dict, PdfName key, double value) {
			dict.Put (key, new PdfNumber (value));
		}
		internal static void Put (this PdfDictionary dict, PdfName key, float value) {
			dict.Put (key, new PdfNumber (value));
		}
		internal static void Put (this PdfDictionary dict, PdfName key, bool value) {
			dict.Put (key, new PdfBoolean (value));
		}
		internal static void Put (this PdfDictionary dict, PdfName key, float[] values) {
			dict.Put (key, new PdfArray (values));
		}
		internal static void Put (this PdfDictionary dict, PdfName key, int[] values) {
			dict.Put (key, new PdfArray (values));
		}

		internal static PdfDictionary CreateDictionaryPath (this PdfDictionary source, params PdfName[] path) {
			PdfDictionary d;
			foreach (var item in path) {
				if (source.Contains (item)) {
					d = source.GetAsDict (item);
					if (d == null) {
						throw new InvalidCastException (item.ToString () + "不是 PdfDictionary。");
					}
					source = d;
					continue;
				}
				d = new PdfDictionary ();
				source.Put (item, d);
				source = d;
				d = null;
			}
			return source;
		}

		internal static bool PdfReferencesAreEqual (PdfIndirectReference r1, PdfIndirectReference r2) {
			if (r1 == null || r2 == null) {
				return false;
			}
			return r1.Number == r2.Number && r1.Generation == r2.Generation;
		}

		internal static int GetPageRotation(PdfDictionary page) {
			return NormalizeRotationNumber (page.TryGetInt32 (PdfName.ROTATE, 0));
		}

		internal static int NormalizeRotationNumber (int rotation) {
			switch (rotation) {
				case 0:
				case 90:
				case 180:
				case 270:
					return rotation;
				default:
					rotation = rotation % 90;
					if (rotation < 0) {
						rotation += 360;
					}
					return (rotation / 90 * 90);
			}
		}

		internal static IList<PdfObject> GetObjectDirectOrFromContainerArray (this PdfDictionary d, PdfName name, int pdfObjectType) {
			var results = new List<PdfObject> ();

			PdfObject tmp;
			tmp = d.Get (name);
			if (tmp == null) {
			}
			else if (tmp.Type == pdfObjectType) {
				results.Add (tmp);
			}
			else if (tmp.Type == PdfObject.ARRAY) {
				var a = tmp as PdfArray;
				for (int i = 0; i < a.Size; i++) {
					tmp = a.GetDirectObject (i);
					if (tmp == null) {
						continue;
					}
					if (tmp.Type == pdfObjectType) {
						results.Add (tmp);
					}
				}
			}
			return results;
		}

		internal static string MatrixToString (iTextSharp.text.pdf.parser.Matrix ctm) {
			return String.Join (" ", ctm[0].ToText(), ctm[1].ToText(), ctm[3].ToText(), ctm[4].ToText(), ctm[6].ToText(), ctm[7].ToText());
		}

		/// <summary>
		/// 获取未使用对象的列表。
		/// </summary>
		/// <param name="pdf">需要检查的 PDF 文档。</param>
		/// <param name="partial">待检查 PDF 文档是否为部分加载。</param>
		/// <returns>包含未使用对象索引值的列表。</returns>
		internal static List<int> ListUnusedObjects (PdfReader pdf, bool partial) {
			var hits = new bool[pdf.XrefSize];
			GetUnusedNode (pdf, partial, pdf.Trailer, hits);
			int i = 0;
			var result = new List<int> ();
			foreach (var item in hits) {
				if (item == false && i > 0) {
					result.Add (i);
				}
				++i;
			}
			return result;
		}

		internal static void SafeSetPageContent (this PdfReader pdf, int pageNumber, byte[] content) {
			var p = pdf.GetPageN (pageNumber);
			var c = p.GetDirectObject (PdfName.CONTENTS);
			if (c != null && c.IsArray ()) {
				var cs = new PRStream (pdf, content);
				if (content.Length > 30) {
					cs.FlateCompress ();
				}
				p.Put (PdfName.CONTENTS, pdf.AddPdfObject (cs));
			}
			else {
				pdf.SetPageContent (pageNumber, content);
			}
		}

		private static void GetUnusedNode (PdfReader pdf, bool partial, PdfObject obj, bool[] hits) {
			var state = new Stack<object> ();
			state.Push (obj);
			int oc = pdf.XrefSize;
			while (state.Count != 0) {
				var current = state.Pop ();
				if (current == null)
					continue;
				List<PdfObject> ar = null;
				PdfDictionary dic = null;
				PdfName[] keys = null;
				Object[] objs = null;
				int idx = 0;
				if (current is PdfObject pdfObject) {
					obj = pdfObject;
					switch (obj.Type) {
						case PdfObject.DICTIONARY:
						case PdfObject.STREAM:
							dic = (PdfDictionary)obj;
							keys = new PdfName[dic.Size];
							dic.Keys.CopyTo (keys, 0);
							break;
						case PdfObject.ARRAY:
							ar = ((PdfArray)obj).ArrayList;
							break;
						case PdfObject.INDIRECT:
							var refi = (PRIndirectReference)obj;
							int num = refi.Number;
							if (!hits[num]) {
								hits[num] = true;
								state.Push (PdfReader.GetPdfObjectRelease (refi));
							}
							continue;
						default:
							continue;
					}
				}
				else {
					objs = (object[])current;
					if (objs[0] is List<PdfObject>) {
						ar = (List<PdfObject>)objs[0];
						idx = (int)objs[1];
					}
					else {
						keys = (PdfName[])objs[0];
						dic = (PdfDictionary)objs[1];
						idx = (int)objs[2];
					}
				}
				if (ar != null) {
					for (int k = idx; k < ar.Count; ++k) {
						var v = ar[k];
						if (v.IsIndirect ()) {
							int num = ((PRIndirectReference)v).Number;
							if (num >= oc || (!partial && pdf.GetPdfObjectRelease (num) == null)) {
								ar[k] = PdfNull.PDFNULL;
								continue;
							}
						}
						if (objs == null) {
							state.Push (new object[] { ar, k + 1 });
						}
						else {
							objs[1] = k + 1;
							state.Push (objs);
						}
						state.Push (v);
						break;
					}
				}
				else {
					for (int k = idx; k < keys.Length; ++k) {
						var key = keys[k];
						var v = dic.Get (key);
						if (v.IsIndirect ()) {
							int num = ((PRIndirectReference)v).Number;
							if (num >= oc || (!partial && pdf.GetPdfObjectRelease (num) == null)) {
								dic.Put (key, PdfNull.PDFNULL);
								continue;
							}
						}
						if (objs == null) {
							state.Push (new object[] { keys, dic, k + 1 });
						}
						else {
							objs[2] = k + 1;
							state.Push (objs);
						}
						state.Push (v);
						break;
					}
				}
			}
		}

	}
}
